﻿/*
 * Copyright 2011 Edward L. Stauff.                     Contact: <EdStauff@gmail.com>.
 * 
 * This file, "CmdLineParam.cs", is part of the "Stauffware.Common.Console" class 
 * library.  "Stauffware.Common" is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Lesser Public License as published by the 
 * Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common" is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * "Stauffware.Common".  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Configuration;
using Stauffware.Common.Core;

namespace Stauffware.Common.Console
{
	//#################################################################################
	/// <summary>
	/// Represents one command-line parameter whose target value is a Field.
	/// </summary>

	internal abstract class FieldParam : CmdLineParam
	{
		protected readonly FieldInfo FieldInfo;

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Factory method
		/// </summary>
		/// <param name="group">the group to which the new parameter belongs</param>
		/// <param name="clpAttr">the custom attribute on the member, or null</param>
		/// <param name="fieldInfo">the FieldInfo for the member, or null</param>
		/// <returns></returns>

		public static FieldParam Create (CmdLineParamGroup group,
									CmdLineArgAttribute clpAttr, FieldInfo fieldInfo)
		{
			FieldParam param;

			if (fieldInfo.FieldType == typeof(bool))
				param = new NiladicFieldParam(group, clpAttr, fieldInfo);
			else if (fieldInfo.FieldType.IsArray)
				param = new ArrayFieldParam(group, clpAttr, fieldInfo);
			else if (typeof(System.Collections.IList).IsAssignableFrom(fieldInfo.FieldType) &&
				fieldInfo.FieldType.IsGenericType)
				param = new ListFieldParam(group, clpAttr, fieldInfo);
			else
				param = new ScalarFieldParam(group, clpAttr, fieldInfo);

			Assert.NotNull(param.EffectiveValueType);
		//	Assert.NotNull(param.ValueParser);

			return param;
		}

		//-----------------------------------------------------------------------------

		protected FieldParam (CmdLineParamGroup group, CmdLineArgAttribute clpAttr, FieldInfo fieldInfo)
			: base(group, clpAttr, fieldInfo)
		{
			Require.NotNull(fieldInfo);
			this.FieldInfo = fieldInfo;
		}

		//-----------------------------------------------------------------------------

		internal override string GetValueString ( )
		{
			return ErrorUtils.ValueToString(this.FieldInfo.GetValue(this.ParamGroup.ParamsObject));
		}
	}

	//#################################################################################
	/// <summary>
	/// Represents one command-line parameter that takes no value on the command line.
	/// The field type is a boolean that tells whether the parameter appeared on the
	/// command line at all.
	/// </summary>

	internal class NiladicFieldParam : FieldParam
	{
		public override bool HasValue { get { return true; } }

		internal NiladicFieldParam (CmdLineParamGroup group, CmdLineArgAttribute clpAttr, FieldInfo fieldInfo)
			: base(group, clpAttr, fieldInfo)
		{
			Require.Condition(fieldInfo.FieldType == typeof(bool), 
										"NiladicFieldParam requires a boolean field");
			this.Arity = Arity.None;
			this.SetValueType(fieldInfo.FieldType);
		}

		//-----------------------------------------------------------------------------

		internal override void SetValue (object value) //, object targetObj)
		{
			this.FieldInfo.SetValue(this.ParamGroup.ParamsObject, value);
		}

	}

	//#################################################################################
	/// <summary>
	/// Represents one command-line parameter whose target value is a Field containing 
	/// a "scalar" value.
	/// </summary>

	internal class ScalarFieldParam : FieldParam
	{
		internal ScalarFieldParam (CmdLineParamGroup group, CmdLineArgAttribute clpAttr, FieldInfo fieldInfo)
			: base(group, clpAttr, fieldInfo)
		{
			this.Arity = Arity.Single;

			if (fieldInfo.FieldType.IsNullable())
				this.SetValueType(fieldInfo.FieldType, fieldInfo.FieldType.GetGenericArguments()[0]);
			else this.SetValueType(fieldInfo.FieldType);
	
			this.ValueParser = this.GetValueParser(clpAttr, fieldInfo, this.EffectiveValueType);
		}

		//-----------------------------------------------------------------------------

		public override bool HasValue
		{
			get 
			{
				return this.FieldInfo.GetValue(this.ParamGroup.ParamsObject) != null;
			}
		}

		//-----------------------------------------------------------------------------

		internal override void SetValue (object value) //, object targetObj)
		{
			this.FieldInfo.SetValue(this.ParamGroup.ParamsObject, value);
		}

	}

	//#################################################################################
	/// <summary>
	/// Represents one command-line parameter whose target value is a Field containing 
	/// an array.
	/// </summary>

	internal class ArrayFieldParam : FieldParam
	{
		private readonly PropertyInfo Length_property;
		private readonly MethodInfo SetValue_method;

		private static readonly Type[] TypesForSetValue = new Type[2] { typeof(object), typeof(int) };

		//-----------------------------------------------------------------------------

		internal ArrayFieldParam (CmdLineParamGroup group, CmdLineArgAttribute clpAttr, FieldInfo fieldInfo)
			: base(group, clpAttr, fieldInfo)
		{
			this.Arity = Arity.Multiple;
			this.SetValueType(fieldInfo.FieldType, fieldInfo.FieldType.GetElementType());
			this.ValueParser = this.GetValueParser(clpAttr, fieldInfo, this.EffectiveValueType);

			this.Length_property = this.FieldInfo.FieldType.GetProperty("Length");
			Assert.NotNull(this.Length_property, fieldInfo.Name + " : Can't find 'Length' property");

			this.SetValue_method = this.FieldInfo.FieldType.GetMethod("SetValue", TypesForSetValue);
			Assert.NotNull(this.SetValue_method, fieldInfo.Name + " : Can't find 'SetValue' method");
		}

		//-----------------------------------------------------------------------------

		public override bool HasValue 
		{ 
			get 
			{
				Array a = this.FieldInfo.GetValue(this.ParamGroup.ParamsObject) as Array;
				return (a != null) && (a.Length > 0);
			} 
		}

		//-----------------------------------------------------------------------------

		internal override void SetValue (object newValue) // , object targetObj)
		{
			object oldArray = this.FieldInfo.GetValue(this.ParamGroup.ParamsObject);
			int oldLength;
			object newArray;

			if (oldArray == null)
				oldLength = 0;
			else oldLength = (int) this.Length_property.GetValue(oldArray, null);

			newArray = Array.CreateInstance(this.EffectiveValueType, oldLength+1);

			if (oldArray != null)
				Array.Copy((Array) oldArray, (Array) newArray, oldLength);

			object[] valuesForSetValue = new object[2] { newValue, oldLength };
			this.SetValue_method.Invoke(newArray, valuesForSetValue);

			this.FieldInfo.SetValue(this.ParamGroup.ParamsObject, newArray);
		}

		//-----------------------------------------------------------------------------
	}

	//#################################################################################
	/// <summary>
	/// Represents one command-line parameter whose target value is a Field and whose
	/// type is a generic IList.
	/// </summary>

	internal class ListFieldParam : FieldParam
	{
		private readonly MethodInfo Add_method;

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="group">the group to which the new parameter belongs</param>
		/// <param name="clpAttr">the custom attribute on the member, or null</param>
		/// <param name="fieldInfo">the FieldInfo for the member, or null</param>

		internal ListFieldParam (CmdLineParamGroup group, CmdLineArgAttribute clpAttr, FieldInfo fieldInfo)
			: base(group, clpAttr, fieldInfo)
		{
			this.Arity = Arity.Multiple;
			Type memberValueType = fieldInfo.FieldType;

			this.Add_method = memberValueType.GetMethod("Add");
			Assert.NotNull(this.Add_method, memberValueType.Name + " : Can't find 'Add' method");
			ParameterInfo[] pi = this.Add_method.GetParameters();
			Assert.Condition(pi.Length == 1, "expecting " + memberValueType.Name + " to take 1 parameter");
			this.SetValueType(fieldInfo.FieldType, pi[0].ParameterType);
			this.ValueParser = this.GetValueParser(clpAttr, fieldInfo, this.EffectiveValueType);
		}

		//-----------------------------------------------------------------------------

		public override bool HasValue
		{
			get
			{
				ICollection c = this.FieldInfo.GetValue(this.ParamGroup.ParamsObject) as ICollection;
				return (c != null) && (c.Count > 0);
			}
		}

		//-----------------------------------------------------------------------------

		internal override void SetValue (object value) // , object targetObj)
		{
			object target = this.FieldInfo.GetValue(this.ParamGroup.ParamsObject);
			if (target == null)
			{
				target = this.FieldInfo.FieldType.GetConstructor(Type.EmptyTypes).Invoke(null);
				this.FieldInfo.SetValue(this.ParamGroup.ParamsObject, target);

				//throw new Exception("can't add an item to the collection " +
				//                     target.GetType().FullName + "." + this.FieldInfo.Name +
				//                    " because its value is null");
			}

			this.Add_method.Invoke(target, new object[] { value });
		}

		//-----------------------------------------------------------------------------
	}

	//#################################################################################
	/// <summary>
	/// Represents one command-line parameter whose target value is a Property.
	/// </summary>

	internal abstract class PropertyParam : CmdLineParam
	{
		protected readonly PropertyInfo PropertyInfo;

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Factory method
		/// </summary>
		/// <param name="group"></param>
		/// <param name="clpAttr"></param>
		/// <param name="propInfo"></param>
		/// <returns></returns>

		public static PropertyParam Create (CmdLineParamGroup group,
									CmdLineArgAttribute clpAttr, PropertyInfo propInfo)
		{
			PropertyParam param;

			if (propInfo.PropertyType == typeof(bool))
				param = new NiladicPropertyParam(group, clpAttr, propInfo);
			else if (propInfo.PropertyType.IsArray)
				param = new ArrayPropertyParam(group, clpAttr, propInfo);
			else if (typeof(System.Collections.ICollection).IsAssignableFrom(propInfo.PropertyType))
				param = new ListPropertyParam(group, clpAttr, propInfo);
			else param = new ScalarPropertyParam(group, clpAttr, propInfo);

			Assert.NotNull(param.EffectiveValueType);
		//	Assert.NotNull(param.ValueParser);

			return param;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="group">the group to which the new parameter belongs</param>
		/// <param name="clpAttr">the custom attribute on the member, or null</param>
		/// <param name="propertyInfo">the PropertyInfo for the member, or null</param>

		protected PropertyParam (CmdLineParamGroup group, CmdLineArgAttribute clpAttr, PropertyInfo propertyInfo)
			: base(group, clpAttr, propertyInfo)
		{
			Require.NotNull(propertyInfo);
			this.PropertyInfo = propertyInfo;

			//Type memberValueType = propertyInfo.PropertyType;

			//if (typeof(System.Collections.IList).IsAssignableFrom(this.ValueType))
			//{
			//    this.Arity = Arity.Multiple;
			//    MethodInfo adder = memberValueType.GetMethod("Add");
			//    Assert.NotNull(adder, memberValueType.Name + " : Can't find 'Add' method");
			//    ParameterInfo[] pi = adder.GetParameters();
			//    Assert.Condition(pi.Length == 1, "expecting " + memberValueType.Name + " to take 1 parameter");
			//    this.ValueType = pi[0].ParameterType;
			//}
			//else
			//{
			//    this.Arity = Arity.Single;
			//    this.ValueType = memberValueType;
			//}

			////	if (this.Arity != Arity.None)
			//this.ValueParser = this.GetValueParser(clpAttr, propertyInfo, this.ValueType);
		}

		//-----------------------------------------------------------------------------

		internal override void SetValue (object value) // , object targetObj)
		{
			this.PropertyInfo.SetValue(this.ParamGroup.ParamsObject, value, null);
		}

		//-----------------------------------------------------------------------------

		internal override string GetValueString ( )
		{
			return ErrorUtils.ValueToString(this.PropertyInfo.GetValue(this.ParamGroup.ParamsObject, null));
		}
	}

	//#################################################################################
	/// <summary>
	/// Represents one command-line parameter that takes no value on the command line.
	/// The property type is a boolean that tells whether the parameter appeared on the
	/// command line at all.
	/// </summary>

	internal class NiladicPropertyParam : PropertyParam
	{
		public override bool HasValue { get { return true; } }

		internal NiladicPropertyParam (CmdLineParamGroup group, CmdLineArgAttribute clpAttr, PropertyInfo propInfo)
			: base(group, clpAttr, propInfo)
		{
			Require.Condition(propInfo.PropertyType == typeof(bool),
										"NiladicPropertyParam requires a boolean field");
			this.Arity = Arity.None;
			this.SetValueType(propInfo.PropertyType);
		}

		//-----------------------------------------------------------------------------

		internal override void SetValue (object value) //, object targetObj)
		{
			this.PropertyInfo.SetValue(this.ParamGroup.ParamsObject, value, null);
		}

	}

	//#################################################################################
	/// <summary>
	/// Represents one command-line parameter whose target value is a Property containing 
	/// a "scalar" value.
	/// </summary>

	internal class ScalarPropertyParam : PropertyParam
	{
		internal ScalarPropertyParam (CmdLineParamGroup group, CmdLineArgAttribute clpAttr, PropertyInfo propInfo)
			: base(group, clpAttr, propInfo)
		{
			this.Arity = Arity.Single;
			
			if (propInfo.PropertyType.IsNullable())
				this.SetValueType(propInfo.PropertyType, propInfo.PropertyType.GetGenericArguments()[0]);
			else this.SetValueType(propInfo.PropertyType);
			
			this.ValueParser = this.GetValueParser(clpAttr, propInfo, this.EffectiveValueType);
		}

		//-----------------------------------------------------------------------------

		public override bool HasValue
		{
			get
			{
				return this.PropertyInfo.GetValue(this.ParamGroup.ParamsObject, null) != null;
			}
		}

		//-----------------------------------------------------------------------------

		internal override void SetValue (object value) //, object targetObj)
		{
			this.PropertyInfo.SetValue(this.ParamGroup.ParamsObject, value, null);
		}

	}

	//#################################################################################
	/// <summary>
	/// Represents one command-line parameter whose target value is a Property containing 
	/// an array.
	/// </summary>

	internal class ArrayPropertyParam : PropertyParam
	{
		private readonly PropertyInfo Length_property;
		private readonly MethodInfo SetValue_method;

		private static readonly Type[] TypesForSetValue = new Type[2] { typeof(object), typeof(int) };

		//-----------------------------------------------------------------------------

		internal ArrayPropertyParam (CmdLineParamGroup group, CmdLineArgAttribute clpAttr, PropertyInfo propInfo)
			: base(group, clpAttr, propInfo)
		{
			this.Arity = Arity.Multiple;
			this.SetValueType(propInfo.PropertyType.GetElementType());
			this.ValueParser = this.GetValueParser(clpAttr, propInfo, this.EffectiveValueType);
			
			this.Length_property = this.PropertyInfo.PropertyType.GetProperty("Length");
			Assert.NotNull(this.Length_property, propInfo.Name + " : Can't find 'Length' property");
			
			this.SetValue_method = this.PropertyInfo.PropertyType.GetMethod("SetValue", TypesForSetValue);
			Assert.NotNull(this.SetValue_method, propInfo.Name + " : Can't find 'SetValue' method");
		}

		//-----------------------------------------------------------------------------

		public override bool HasValue
		{
			get
			{
				Array a = this.PropertyInfo.GetValue(this.ParamGroup.ParamsObject, null) as Array;
				return (a != null) && (a.Length > 0);
			}
		}

		//-----------------------------------------------------------------------------

		internal override void SetValue (object newValue) // , object targetObj)
		{
			object oldArray = this.PropertyInfo.GetValue(this.ParamGroup.ParamsObject, null);
			int oldLength;
			object newArray;

			if (oldArray == null)
				oldLength = 0;
			else oldLength = (int) this.Length_property.GetValue(oldArray, null);

			newArray = Array.CreateInstance(this.EffectiveValueType, oldLength+1);

			if (oldArray != null)
				Array.Copy((Array) oldArray, (Array) newArray, oldLength);

			object[] valuesForSetValue = new object[2] { newValue, oldLength };
			this.SetValue_method.Invoke(newArray, valuesForSetValue);

			this.PropertyInfo.SetValue(this.ParamGroup.ParamsObject, newArray, null);
		}

		//-----------------------------------------------------------------------------

	}

	//#################################################################################
	/// <summary>
	/// Represents one command-line parameter whose target value is a Property and whose
	/// type is an ICollection.
	/// </summary>

	internal class ListPropertyParam : PropertyParam
	{
		private readonly MethodInfo Add_method;

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="group">the group to which the new parameter belongs</param>
		/// <param name="clpAttr">the custom attribute on the member, or null</param>
		/// <param name="propInfo">the PropertyInfo for the member, or null</param>

		internal ListPropertyParam (CmdLineParamGroup group, CmdLineArgAttribute clpAttr, PropertyInfo propInfo)
			: base(group, clpAttr, propInfo)
		{
			this.Add_method = propInfo.PropertyType.GetMethod("Add");
			Assert.NotNull(this.Add_method, propInfo.Name + " : Can't find 'Add' method");

			this.Arity = Arity.Multiple;

			Type memberValueType = propInfo.PropertyType;
			this.Add_method = memberValueType.GetMethod("Add");
			Assert.NotNull(this.Add_method, memberValueType.Name + " : Can't find 'Add' method");
			ParameterInfo[] pi = this.Add_method.GetParameters();
			Assert.Condition(pi.Length == 1, "expecting " + memberValueType.Name + " to take 1 parameter");
			this.SetValueType(propInfo.PropertyType, pi[0].ParameterType);
			this.ValueParser = this.GetValueParser(clpAttr, propInfo, this.EffectiveValueType);
		}

		//-----------------------------------------------------------------------------

		public override bool HasValue
		{
			get
			{
				ICollection c = this.PropertyInfo.GetValue(this.ParamGroup.ParamsObject, null) as ICollection;
				return (c != null) && (c.Count > 0);
			}
		}

		//-----------------------------------------------------------------------------

		internal override void SetValue (object value) // , object targetObj)
		{
			object target = this.PropertyInfo.GetValue(this.ParamGroup.ParamsObject, null);
			if (target == null)
			{
				target = this.PropertyInfo.PropertyType.GetConstructor(Type.EmptyTypes).Invoke(null);
				this.PropertyInfo.SetValue(this.ParamGroup.ParamsObject, target, null);

				//throw new Exception("can't add an item to the collection " +
				//                     target.GetType().FullName + "." + this.PropertyInfo.Name +
				//                    " because its value is null");
			}
			
			this.Add_method.Invoke(target, new object[] { value });
		}

		//-----------------------------------------------------------------------------
	}

	//#################################################################################
}
